{-
Jeff Ahern & kevin Gin
CSC 372
Homework 3B
TurnItOn
-}
import Char
import IO

-- =================================================================
-- Utility functions
-- =================================================================

-----------------------------------------------------------------
-- The function call
--   windows size list
-- where
--   size = the size of the window
--   list = a list of elements of arbitrary type
-- will return a list of sublists of 'list',
-- constructed by sweeping a window of size 'size'
-- over list. 
-- For example, if 
--    list = [1,2,3,4,5]
--    size = 2
-- the we return
--    [[1,2],[2,3],[3,4],[4,5]
-- but if 
--    size = 3
-- we return
--    [[1,2,3],[2,3,4],[3,4,5]]
windows :: Int -> [a] -> [[a]]
windows k xs  = (map (take k).takeWhile (\ x -> length x >= k).iterate (drop 1)) xs 

-----------------------------------------------------------------
-- The function call
--    mapAllPairs f xs
-- where
--    f    = a binary function
--    xs   = a list of elements of arbitrary type
-- returns the result of applying f
--    to all pairs of elemtents drawn from xs.
-- Thus, this function, essentially, computes the
-- cross product of xs with itself, and applies 
-- f to all the resulting pairs of elements. 
mapAllPairs :: (a -> a -> b) -> [a] -> [b]
mapAllPairs f xs = [ f x y | x<-xs, y<-xs ]

-- =================================================================
-- Comparisons
-- =================================================================
intCMP :: Int -> Int -> Ordering
intCMP a b | a == b = EQ 
           | a < b  = LT
           | otherwise = GT

strCMP :: String -> String -> Ordering
strCMP a b | a == b = EQ 
           | a < b  = LT
           | otherwise = GT

doubleCMPRev :: Double -> Double -> Ordering
doubleCMPRev a b | a == b    = EQ 
                 | a < b     = GT
                 | otherwise = LT


-- =================================================================
-- Operations on sets
-- =================================================================
isSet  :: Ord a => (a -> a-> Ordering) -> [a] -> Bool
isSet cmp xs | xs == [] = True
	     | otherwise = if length xs == length (foldr order [] xs) then True else False
	where
		order = (\y ys -> if ys == [] then y : ys else if cmp y (head ys) == EQ || cmp y (head ys) == GT then ys else y : ys)

-----------------------------------------------------------------
-- Return true if x occurs in xs using cmp as the comparison function
member :: Ord a => (a -> a-> Ordering) -> a -> [a] -> Bool
member cmp x xs | isSet cmp xs == False = error "Arguments must be sets!"
		| xs == [] = False
		| x == head xs = True
		| otherwise = if length (foldr z [] xs) < length xs then True else False 
	where
		z =  (\y ys -> if ys == [] then y : ys else if cmp x (head ys) == EQ then ys else y : ys)  

-----------------------------------------------------------------
-- Make a set from a list by removing duplicates and sorting
makeSet :: Ord a => (a -> a-> Ordering) -> [a] -> [a]
makeSet cmp xs | xs == [] = xs
			   | otherwise = remdups (msort cmp xs)

-----------------------------------------------------------------
-- Return the set intersection of two sets (previously constructed
-- by makeSet), using cmp as the element comparison function.
setIntersect  :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> [a]
setIntersect cmp xs ys | xs == [] || ys == [] = []
					   | isSet cmp xs == False || isSet cmp ys == False = error "arguments must be sets"
					   | inSet cmp (head xs) ys = head xs : setIntersect cmp (tail xs) (ys)
					   | otherwise = setIntersect cmp (tail xs) ys

-----------------------------------------------------------------
-- Return the set union of two sets (previously constructed
-- by makeSet), using cmp as the element comparison function.
setUnion  :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> [a]
setUnion cmp xs ys  | xs == [] = ys
					| ys == [] = xs
					| isSet cmp xs == False || isSet cmp ys == False = error "arguments must be sets"
					| cmp (head xs) (head ys) == LT = head xs : setUnion cmp (tail xs) ys 
					| cmp (head ys) (head xs) == LT = head ys : setUnion cmp xs (tail ys) 
					| cmp (head ys) (head xs) == EQ = head xs : setUnion cmp (tail xs) (tail ys)

setSubtract  :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> [a]
setSubtract cmp xs ys | xs == [] || ys == [] = []
					  | isSet cmp xs == False || isSet cmp ys == False = error "arguments must be sets"
					  | otherwise = remove cmp (head (setIntersect cmp xs ys)) xs

setIsSubset  :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> Bool
setIsSubset cmp xs ys | isSet cmp xs == False || isSet cmp ys == False = error "arguments must be sets"
					  | setIntersect cmp xs ys == xs = True
					  | otherwise = False

remove :: Ord a => (a -> a-> Ordering) -> a -> [a] -> [a]				  
remove cmp x xs | xs == [] = xs
				| x == head xs = remove cmp x (tail xs)
				| otherwise = head xs : (remove cmp x (tail xs))

-----------------------------------------------------------------
-- Compute the similarity (a value between 0.0 and 1.0) between two
-- sets. If both sets are empty, return 0.0.
setSimilarity :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> Double
setSimilarity cmp xs ys	| isSet cmp xs == False || isSet cmp ys == False = error "arguments must be sets"
						| otherwise = fromIntegral (length ( (setIntersect cmp xs ys) ) )   / fromIntegral (length ( setUnion cmp xs ys ) )

						
{- Msort takes in a list and returns the list sorted using mergesort. -}
msort :: Ord a => (a -> a-> Ordering) -> [a] -> [a]
msort cmp xs | xs == [] = []
			 | length xs == 1 = xs
			 | length xs == 2 = merge cmp (head xs : []) (tail xs)
			 | (length xs) `mod` 2 == 0 = merge cmp (msort cmp (take a xs) ) (msort cmp (drop a xs) )
			 | (length xs) `mod` 2 /= 0 = merge cmp (msort cmp (take b xs) ) (msort cmp (drop b xs) )
					where
						a = (length xs `div` 2)
						b = ( (length xs) `div` 2) + 1
{- Helper method taken from lecture slides that removes duplicates. -}
remdups :: Eq a => [a] -> [a]
remdups [] = []
remdups [x] = [x]
remdups (x:y:ys) | x == y = remdups (x:ys)
 				  | otherwise = x : remdups (y:ys)
	

{- InSet takes in a value and a List and checks to see if that value is in that list.  True if value is
 in the set, false otherwise. -}
inSet :: Ord a => (a -> a-> Ordering) -> a -> [a] -> Bool
inSet cmp x xs  | xs == [] = False
				| isSet cmp xs == False = error "arguments must be sets"
				| cmp x (head xs) == EQ = True
		   		| otherwise = inSet cmp x (tail xs)

{- Merge takes two lists and puts them and returns one merged and sorted list. -}
merge :: Ord a => (a -> a-> Ordering) -> [a] -> [a] -> [a]
merge cmp xs ys | xs == [] && ys == [] = []
				| xs == [] = ys
				| ys == [] = xs
				| (cmp (head xs) (head ys) ) == EQ = (head xs) : (head ys) : merge cmp (tail xs) (tail ys)
				| (cmp (head xs) (head ys) ) == LT = (head xs) : merge cmp (tail xs) ys
				| (cmp (head xs) (head ys) ) == GT = (head ys) : merge cmp xs (tail ys)				
-- =================================================================
-- Hashes are represented as Ints.
-- =================================================================

type Hash = Int
type HashSet = [Hash]

hash :: String -> Hash
hash xs = abs( foldr (\ x y -> ( ord x ) + 3*y ) 1 xs )

-- =================================================================
-- A document is represented by its filename (a String).
-- When we've read in a document it is represented as a
-- list of the lines read, plus the line number.
-- =================================================================

-----------------------------------------------------------------
-- The name of a document is it's path, a String.
type DocName = FilePath

-----------------------------------------------------------------
-- A line of text, as read from a file, is represented by
-- a (row-number,line) tuple. The row-number starts at 0.
type Line = (Int,String)

-----------------------------------------------------------------
-- A document, as read from a file, is represented by
-- a list of (row-number,line) tuples.
type Document = [Line]

-- =================================================================
-- Functions for reading text files and splitting them
-- into lists of lines.
-- =================================================================

-----------------------------------------------------------------
-- Given a string with embedded newlines, splitLines
-- will split it into a list of (row-number, line)
-- tuples.
splitLines :: String -> Document
splitLines "" = []
splitLines s = zip [0..] (splitLinesHelper s)

splitLinesHelper s | s == "" = []
			       | otherwise = ( nextnl s) : splitLinesHelper(afternl s)

-- Gives the string before the first newline char
nextnl s | s == "" = ""
		 | head(s) == '\n' = ""
         | otherwise = head(s) : nextnl(tail(s))

-- Gives the string after the first newline char
afternl s | s == "" = ""
		  | head(s) == '\n' = tail(s)
          | otherwise = afternl(tail(s))




-----------------------------------------------------------------
-- Given a list of filenames, loadFiles will read 
-- in the files and return them as a list of 
-- tuples (file-name,file-contents).
loadFiles :: [DocName] -> IO [(DocName,Document)]
loadFiles [] = return []
loadFiles (x:xs) = do 
                cont <- readFile x 
                let lines = splitLines cont
                tail <- loadFiles xs
                let files = (x,lines):tail
                return files
				

-- =================================================================
-- A Position is the location of a character in a file,
-- represented by the tuple (line-number, column-number).
-- =================================================================
type Position = (Int, Int)

-- =================================================================
-- A shingle is a substring of the original document
-- plus the position at which it occurs. This section
-- contains functions for computing the shingles of
-- a document.
-- =================================================================

type Shingle = (String, Position)

-----------------------------------------------------------------
-- Extract all the singles from a line of text by 
-- sweeping a window of size shingleSize across it.
line2shingles :: Int -> Line -> [Shingle]
line2shingles shingleSize (lineNumber,text) = zip (windows shingleSize text) (zip (makeIndefList lineNumber) [0..])
	where makeIndefList c = (c) : makeIndefList c



-----------------------------------------------------------------
-- Extract all the shingles from a document
-- by sweeping a window of size windowSize
-- over each of the lines of text.
shingles :: Int -> (DocName,Document) -> (DocName,[Shingle])
shingles shingleSize (doc, xs) = (doc, shinglesHelper shingleSize xs)

shinglesHelper :: Int -> Document -> [Shingle]
shinglesHelper shingleSize [] = []
shinglesHelper shingleSize (x:xs) = (line2shingles shingleSize x) ++ (shinglesHelper shingleSize xs)

-----------------------------------------------------------------
-- Given a list of shingles, convert it to a set of
-- hash values using the function "hash" above. There 
-- can be multiple shingles with the same string value, 
-- mapping to the same hash value, but the output of this 
-- function is a *set*, i.e. an ordered set of unique hashes.
shingles2hashSet :: (DocName,[Shingle]) -> (DocName,HashSet)
shingles2hashSet (doc,xs) = (doc,  makeSet intCMP (shingles2hashList xs) )

shingles2hashList :: [Shingle] -> HashSet
shingles2hashList [] = []
shingles2hashList (x:xs) = ( hash (takeL x)) : shingles2hashList xs
		where takeL (a,b) = a

-- =================================================================
-- This section contains algorithms for winnowing down sets of hashes 
-- to smaller sets. Usually, documents are large, and will result in 
-- way too many hashes being generated. "Winnowing" is any technique for
-- reducing the number of hashes. Here, we give several winnowing
-- functions. They all take a list of shingles as input and return a
-- (usually smaller) list of shingles. Later on, we can use
-- the most appropriate winnowing function depending on our
-- needs.
-- =================================================================
type WinnowFun = (DocName,[Shingle]) -> (DocName,[Shingle])

-----------------------------------------------------------------
-- The identity function, returning the same list of
-- shingles as it is given.
winnowNone :: (DocName,[Shingle]) -> (DocName,[Shingle])
winnowNone (d,xs) = (d,xs)

-----------------------------------------------------------------
-- Return only those shingles whose hash values is "0"
-- mod p.
winnowModP :: Int -> (DocName,[Shingle]) -> (DocName,[Shingle])
winnowModP p (a,b) = (a, (filter (\x -> ((hash (fst x)) `mod` p) == 0) b))

-----------------------------------------------------------------
-- Sweep a window of size p across the list of shingles.
-- For every window only keep the one with the minimum value.
-- If there are more than one 
winnowWindow :: Int -> (DocName,[Shingle]) -> (DocName,[Shingle])
winnowWindow p (d,xs) = (d, (snd(unzip ((\x -> minimum x) (windows p (zip (snd (shingles2hashSet (d,xs))) xs))))) )
-- =================================================================
-- Main moss algorithm for computing the similarities between
-- n different documents.
-- =================================================================

-----------------------------------------------------------------
-- Given a list of documents (tuples of (docName, hash-set)),
-- compare all pairs of documents for similarity. This function 
-- returns a sorted list of tuples (DocName1,DocName2,SimilarityScore).
-- SimilarityScore is the value returned by the setSimilarity 
-- function over the hash-sets of each pair of documents. The 
-- resulting list is sorted, most similar documents first.
compareAllDocs :: [(DocName,HashSet)] -> [(DocName,DocName,Double)]
compareAllDocs xs = mapAllPairs (\ x y -> ( (fst x), (fst y), setSimilarity intCMP (snd x)  (snd y))) xs

-----------------------------------------------------------------
-- The function call
--    kgram windowSize winnow (name,doc)
-- where
--   windowSize = the size of the window which we will sweep
--                across the document.
--   winnow     = the function which winnows down lists of
--                shingles to a manageable size
--   (name,doc) = the name and the contents of the file we
--                want to process.
-- performs the following steps:
--   1) compute the list of shingles of size windowSize
--   2) winnow down the list of shingles to a manageable size
--   3) convert the list of shingles to a set of hashes.
kgram :: Int -> WinnowFun -> (DocName,Document) -> (DocName,HashSet)
kgram windowSize winnow = shingles2hashSet . winnow . shingles windowSize
-----------------------------------------------------------------
-- The function call
--   moss windowSize winnowFunction listOfDocuments
-- where
--    windowSize      = the size of the windows we sweek across the document
--    winnowFun       = the function that winnows down the large 
--                      list of shingles to a more manageable list
--    listOfDocuments = a list of pairs (document-name, document-contents)
--                      that we want to compare
-- performs the following steps:
--    1) use 'kgram' to compute a set of hashes for each document
--    2) use 'compareAllDocs' to compute, for every pair of documents,
--       how similar they are.
-- This function uses the compareAllDocs function above
-- to compute a list of all pairs of documents and a
-- measure of how similar they are.
moss :: Int -> WinnowFun ->[(DocName,Document)] -> [(DocName,DocName,Double)]
--moss windowSize winnowFun = moss windowSize winnowFun -- Just to pass the typechecker!!
moss windowSize winnowFun = compareAllDocs . map (kgram windowSize winnowFun )
-----------------------------------------------------------------
-- The function call
--    similar shingleSize winnowAlg fileNames
-- performs the actions
--    1) read in all the files in fileNames (a list of strings),
--    2) compute the simiarity of each pair of documents.
-- This function uses compareAllDocs above to do the actual
-- comparisons.
similar :: Int -> WinnowFun -> [DocName] -> IO [(DocName, DocName, Double)]
similar shingleSize winnowAlg fileNames = do
      docs <- loadFiles fileNames
      let w = moss shingleSize winnowAlg docs
      return w

-----------------------------------------------------------------
-- Below are four functions which are all specializations
-- of the "similar" function.
--    1) sim_none does no winnowing.
--    2) sim_modP uses the mod P winnowing algorithm.
--    3) sim_window uses the window-based winnowing algorithm
--    4) sim is like sim_modP but with a default shingle size
--       set to 3 and with winnowing set to mod 4.
sim_none :: Int -> [DocName] ->  IO [(DocName, DocName, Double)]
sim_none shingleSize = similar shingleSize winnowNone 

sim_modP :: Int -> Int -> [DocName] -> IO [(DocName, DocName, Double)]
sim_modP shingleSize modP = similar shingleSize (winnowModP modP) 

sim_window :: Int -> Int -> [DocName] -> IO [(DocName, DocName, Double)]
sim_window shingleSize windowSize = similar shingleSize (winnowWindow windowSize) 

sim :: [DocName] -> IO [(DocName, DocName, Double)]
sim files = sim_modP 3 4 files 

-- =================================================================
-- This section assumes that we've already found which documents
-- are similar using the "similar" function above. We now 
-- want to display two files side by side with similarities
-- color-coded.
-- =================================================================

-----------------------------------------------------------------
-- This is the main function for visualizing the
-- similarities between two documents.
-- The functio  call
--   visualize doc1name doc2name
-- where 
--    doc1name = the name of the first document
--    doc2name = the name of the second document
-- this function will
--    1) read in the files,
--    2) compute their similarities (by comparing the set of shingles),
--    3) compute an html string visualizing the similarities,
--    4) write the html string to the file named
--           doc1name-doc2name.html.
visualize :: Int -> DocName -> DocName -> IO()
visualize shingleSize doc1name doc2name = do
          let outfileName = doc1name ++ "-" ++ doc2name ++ ".html"
          [doc1,doc2] <- loadFiles [doc1name,doc2name] 
          let shingles1 = shingles shingleSize doc1
          let shingles2 = shingles shingleSize doc2
          let common = commonStrings shingles1 shingles2
          let regions1 = shingles2regions shingles1 common
          let regions2 = shingles2regions shingles2 common
          let html = showSimilarities doc1 regions1 doc2 regions2
          writeFile outfileName html

-----------------------------------------------------------------
-- The viz function is a specializion of "visualize" with
-- the shingle size set to 3.
viz :: DocName -> DocName -> IO()
viz = visualize 3

-----------------------------------------------------------------
-- A Region is a tuple (pos,len) that represents 
-- a segment of a file starting in position pos
-- (a (row,column) tuple) and extending for len
-- characters. For simplicity, a Region never
-- extends past the end of a line.
type Region = (Position,Int)

-----------------------------------------------------------------

-- The function call
--   inRegion pos region
-- returns True if pos is within region.
-- Regions don't span lines.
inRegion :: Position -> Region -> Bool
inRegion (row,col) ((r,c),len) = row==r && col>=c && col<(c+len)

-- The function call
--   inRegions pos listOfRegions
-- returns True if pos is within any of the
-- regions in listOfRegions. 
inRegions :: Position -> [Region] -> Bool
inRegions pos reg = or (map (inRegion pos ) reg)

-----------------------------------------------------------------
-- The function call
--   commonStrings (doc1name,shingles1) (doc2name,shingles2)
-- where
--   doc1name  = name of the first document
--   shingles1 = the list of shingles of the first document
--   doc2name  = name of the second document
--   shingles2 = the list of shingles of the second document
-- returns
--   the *set* of the shingle strings that occur in both documents
--   (i.e. compute the *intersection* of the shingle strings in the
--   two documents).
commonStrings :: (DocName,[Shingle]) -> (DocName,[Shingle]) -> [String]
commonStrings (_,shingles1) (_,shingles2) = setIntersect strCMP (makeSet strCMP(fst(unzip shingles1))) (makeSet strCMP (fst(unzip shingles2)))
--commonStrings (_,shingles1) (_shingles2) = []
-----------------------------------------------------------------
-- The function call 
--   shingles2regions (doc,shingles) common
-- where
--   doc      = the name of a document
--   shingles = the list of the shingles of the document
--   common   = a list of strings that occur in this document
--              (and which, eventually, we will want to highlight
--              because they also occur in the document we're
--              comparing too)
-- returns 
--   a list of regions where the strings in common 
--   occur in shingles.
shingles2regions :: (DocName,[Shingle]) -> [String] -> [Region]
shingles2regions (doc,shingles) common = concatregions shingles common
					where concatregions shingles common | common == [] = []
														| otherwise = shingles2region shingles (head(common)) ++ concatregions shingles (tail(common))

shingles2region :: [Shingle] -> String -> [Region]
shingles2region shingles common = map (\(x,y)->(y,length(common))) (filter (\(x, y) -> x == common ) shingles)


-----------------------------------------------------------------
-- The function call
--   showSimilarities (doc1name,doc1) reg1 (doc2name,doc2) reg2
-- where
--    doc1name    = name of the first document
--    doc1        = the text of document 1 as a list of lines
--    reg1        = a list of regions of document 1 that should 
--                  be highlighted
--    doc2name    = name of the second document
--    doc2        = the text of document 2 as a list of lines
--    reg2        = a list of regions of document 2 that should 
--                  be highlighted
-- will perform the actions
--    1) compute a version of doc1 where regions in reg1 
--       have been highlighted
--    2) compute a version of doc2 where regions in reg2 
--       have been highlighted
--    3) construct html code that displays the hightlighted 
--       documents side by side
showSimilarities :: (DocName,Document) -> [Region] -> (DocName,Document) -> [Region] -> String
showSimilarities (doc1name,doc1) reg1 (doc2name,doc2) reg2 = html
  where
     ann1 = highlight doc1 reg1
     ann2 = highlight doc2 reg2
     html = htmlTemplate (doc1name,ann1) (doc2name,ann2)

-----------------------------------------------------------------
-- The function call
--   highlight doc regions 
-- where 
--   doc     = a text document represented as (row,line) pairs
--   regions = a list of ((row,column),length) tuples, 
--             representing the regions of the document that
--             should be highlighted
-- will return a string where each character c whose position
-- lies within any of the regions has been highlighted (i.e.
-- replaced by the output of the
--       highlightChar c
-- function call).
highlight :: Document -> [Region] -> [String]
highlight [] _ = []
highlight (d:docs) reg = highligeHelpper d reg : highlight docs reg


{- used to strip off the line number and filter the regions to that line number -}
highligeHelpper :: Line -> [Region] -> String
highligeHelpper (l, str ) reg = convertString  str (filter (\ ( (a,p),c ) -> a == l ) reg )


{-  Converts the string and region list to a string-}
convertString :: String -> [Region] -> String
convertString str reg = strList2str (machStrings (regions2list reg) (zip str [1..]) )

{- Same thing as unlines, just my implimitation-}
strList2str:: [String] -> String
strList2str (x:xs) | xs == [] = []
                   | otherwise = x ++ strList2str xs


{- Mathces the string location to a reagion position and hilights what needes to be hilighted -}
machStrings:: [Int] -> [(Char,Int)] -> [String]
machStrings nl regionslist = [ if ( inList x nl ) then pairHilight x else pairNoH x | x<-regionslist ]
    where pairHilight (a,b) = highlightChar a
          pairNoH (a,b) = charToString a

{- Checks to see if a number is in a list-}
inList:: (Char,Int) -> [Int] -> Bool
inList a (l:ls) | pairList l a = True
                | ls ==[] = False
                | otherwise = inList a ls 
                    where pairList a (b,n) = a == n

{- Converst a list of regions to a list of numbers -}
regions2list:: [Region] -> [Int]
regions2list regions | regions == [] = []
                     | otherwise = region2list (head regions) ++ regions2list( tail regions)

{- converts a region to a list of numbers that represent positions that should be hilighted-}
region2list :: Region -> [Int]
region2list region = [ x | x<-[start(region)..end(region)] ]

{- what line the region is on -}
regionLine:: Region -> Int
regionLine ((l,s),c) = l

{- finds the start of a region -}
start:: Region -> Int
start ((l,s),c) = s+1

{- finds the end of an region-}
end:: Region -> Int
end ((l,s),c) = s+c

{- Converts the char to a string of lenght 1 -}
charToString :: Char -> String
charToString c = [c]

-----------------------------------------------------------------
-- This function wraps a string in html that
-- gives it a yellow background color.
highlightChar :: Char -> String
highlightChar x = "<FONT style=\"BACKGROUND-COLOR: yellow\">" ++ [x] ++ "</FONT>"

-----------------------------------------------------------------
-- The function call
--   htmlTemplate (doc1name lines1) (doc2name lines2)
-- returns a string containing html that
-- displays lines1 and lines2 side by side.
htmlTemplate :: (DocName,[String]) -> (DocName,[String]) -> String
htmlTemplate (doc1name,lines1) (doc2name,lines2)= "\
   \<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n\
   \<html>\n\
   \  <head>\n\
   \    <title>" 
   ++ 
   title 
   ++ 
   "</title>\n\
   \  </head>\n\
   \\n\
   \  <body>\n\
   \<center>\n\
   \    <h1>"
   ++
   title
   ++
   "</h1>\n\
   \   <table border=1>\n\
   \      <tr>\n\
   \         <th>" ++ doc1name ++ "</th>\n\
   \         <th>" ++ doc2name ++ "</th>\n\
   \      </tr>\n\
   \      <tr>\n\
   \         <td>\n\
   \<pre>\n"
   ++
   unlines lines1 
   ++
   "\n</pre>\n\
   \         </td>\n\
   \         <td>\n\
   \<pre>\n"
   ++
   unlines lines2
   ++
   "\n</pre>\n\
   \         </td>\n\
   \     </tr>\n\
   \   </table>\n\
   \</center>\n\
   \  </body>\n\
   \</html>\n"
   where
      title = doc1name ++ " vs. " ++ doc2name

-- =================================================================
-- Useful for debugging
-- =================================================================

fred0 = shingles 3 ("fred",[(0,"yabbadabbadoo")])
fred0a = shingles 3 ("fred",[(0,"bbadooyabbada")])
frank0 = shingles 3 ("frank",[(0,"doobeedoobeedoo")])
scooby0 = shingles 3 ("scooby",[(0,"scoobydoobydoo")])

fred1 = kgram 3 winnowNone ("fred",[(0,"yabbadabbadoo")])
fred1a = kgram 3 winnowNone ("fred",[(0,"bbadooyabbada")])
frank1 = kgram 3 winnowNone ("frank",[(0,"doobeedoobeedoo")])
scooby1 = kgram 3 winnowNone ("scooby",[(0,"scoobydoobydoo")])

fred2 = kgram 3 (winnowModP 4) ("fred",[(0,"yabbadabbadoo")])
fred2a = kgram 3 (winnowModP 4) ("fred",[(0,"bbadooyabbada")])
frank2 = kgram 3 (winnowModP 4) ("frank",[(0,"doobeedoobeedoo")])
scooby2 = kgram 3 (winnowModP 4) ("scooby",[(0,"scoobydoobydoo")])

sim1 = sim ["Fib1.java","Fibonacci3.java","Fibonacci4.java"]
sim2 = sim ["fred"]
sim3 = sim_none 3 ["fred","frank"] 
sim4 = sim_none 3 ["fred","frank","scooby"]
sim5 = sim []

viz1 = viz "landskrona1" "landskrona3"
viz2 = viz "fred" "frank"
